/*
 * Copyright 2010-2012 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jslet.paas.dynamichelper;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.PostConstruct;

import org.eclipse.persistence.annotations.BatchFetchType;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.dynamic.DynamicClassLoader;
import org.eclipse.persistence.dynamic.DynamicEntity;
import org.eclipse.persistence.dynamic.DynamicType;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.jpa.dynamic.JPADynamicHelper;
import org.eclipse.persistence.jpa.dynamic.JPADynamicTypeBuilder;
import org.eclipse.persistence.mappings.ForeignReferenceMapping;
import org.eclipse.persistence.mappings.OneToOneMapping;
import org.eclipse.persistence.mappings.UnidirectionalOneToManyMapping;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.stereotype.Service;

import org.jslet.paas.common.IsoDate;
import org.jslet.paas.modelmeta.model.FieldMeta;
import org.jslet.paas.modelmeta.model.FieldMetaType;
import org.jslet.paas.modelmeta.model.ModelMeta;
import org.jslet.paas.modelmeta.model.ModelMetaConfiguration;

@Service
public class BizModelHelper
{
	private static final String DEFAULTPACKAGE = "org.jslet.entity";

	private static final Logger logger = LoggerFactory.getLogger(BizModelHelper.class);

	private JPADynamicHelper helper;

	private Map<String, JPADynamicTypeBuilder> typeBuilderList = new HashMap<String, JPADynamicTypeBuilder>();

	@Autowired
	private ApplicationContext applicationContext;

	@PostConstruct
	public void initialize()
	{
		LocalContainerEntityManagerFactoryBean factoryBean = applicationContext
				.getBean(LocalContainerEntityManagerFactoryBean.class);
		this.helper = new JPADynamicHelper(factoryBean.nativeEntityManagerFactory);
		this.loadConfig(this.getAllBizObjectConfiguration());
	}

	/***
	 * 
	 * Get business object configuration.
	 * 
	 * @return
	 */
	public List<ModelMeta> getAllBizObjectConfiguration()
	{
		return ModelMetaConfiguration.SINGLETON.getAllBoConfig();
	}

	/**
	 * 
	 * Load business object type.
	 * 
	 * @param bizCfgs
	 */
	public final void loadConfig(List<ModelMeta> bizCfgs)
	{
		String typeName;
		for (ModelMeta bizCfg : bizCfgs)
		{
			typeName = this.getTypeName(bizCfg);
			this.createType(bizCfg, typeName);
		}
		// for (JPADynamicTypeBuilder typeBuilder : this.typeBuilderList.values())
		// Object[] typeBuilders = this.typeBuilderList.values().toArray();
		// JPADynamicTypeBuilder tb;
		// for(int i = typeBuilders.length - 1; i >= 0; i-- )
		// {
		// tb = (JPADynamicTypeBuilder)typeBuilders[i];
		// this.helper.addTypes(false, false, tb.getType());
		// }
	}

	/**
	 * 
	 * Refresh business object type by BizObject configuration.
	 * 
	 * @param bizCfg
	 */
	public final void refreshBoConfig(ModelMeta bizCfg)
	{
		String typeName = this.getTypeName(bizCfg);
		this.helper.removeType(typeName);
		this.typeBuilderList.remove(bizCfg.getName());
		this.createType(bizCfg, typeName);
		// for (JPADynamicTypeBuilder typeBuilder : this.typeBuilderList.values())
		// {
		// this.helper.addTypes(false, false, typeBuilder.getType());
		// }
	}

	/***
	 * 
	 * Create new empty entity by business object name.
	 * 
	 * @param boName business object name
	 * @return new empty entity
	 */
	public final DynamicEntity createEntity(String boName)
	{
		String typeName = this.getTypeName(boName);
		return this.helper.newDynamicEntity(typeName);
	}

	/***
	 * 
	 * Get key field.
	 * 
	 * @param boName business object name
	 * @return key field
	 */
	public final String getBizObjectKeyField(String boName)
	{
		ModelMeta bizCfg = this.getBizModelByName(boName);
		if (bizCfg == null){
			return null;
		}
		String result = bizCfg.getKeyField();
		return result != null ? result.trim() : null;
	}

	/***
	 * 
	 * Get java class by business object name.
	 * 
	 * @param boName business object name
	 * @return Business object's java class
	 */
	public final Class<?> getClassByBoName(String boName)
	{
		DynamicType bizType = this.helper.getType(this.getTypeName(boName));
		return bizType != null ? bizType.getJavaClass(): null;
	}

	private String getTypeName(String boName)
	{
		ModelMeta bizCfg = this.getBizModelByName(boName);
		if (bizCfg == null)
		{
			return null;
		}
		return this.getTypeName(bizCfg);
	}

	private String getTypeName(ModelMeta bizCfg)
	{
		String packageName = bizCfg.getPackageName();
		String packagePrefix = DEFAULTPACKAGE;
		if (packageName != null && packageName.trim().length() > 0){
			packagePrefix = packageName.endsWith(".") ? packageName : packageName + ".";
		}
		return packagePrefix + bizCfg.getName();
	}

	private ModelMeta getBizModelByName(String boName)
	{
		for (ModelMeta bizObj : this.getAllBizObjectConfiguration())
		{
			if (boName.equalsIgnoreCase(bizObj.getName()))
			{
				return bizObj;
			}
		}
		logger.warn("NOT found bizObject name: " + boName);
		return null;
	}

	private void createType(String boName)
	{
		if (boName == null){
			return;
		}
		ModelMeta currBizObj = this.getBizModelByName(boName);

		if (currBizObj == null)
		{
			return;
		}
		this.createType(currBizObj, null);
	}

	private void createType(ModelMeta bizCfg, String typeName)
	{
		String modelName = bizCfg.getName().toLowerCase();
		if (this.typeBuilderList.containsKey(modelName)){
			return;
		}
		if (typeName == null)
		{
			typeName = this.getTypeName(bizCfg);
		}
		DynamicClassLoader dcl = helper.getDynamicClassLoader();
		Class<?> bizObjectClass = dcl.createDynamicClass(typeName);
		// One entity can map to multiple db tables(separated by ",")
		String[] dbtabls = bizCfg.getDbObject().split(",");
		JPADynamicTypeBuilder bizTypeBuilder = new JPADynamicTypeBuilder(bizObjectClass, null, dbtabls);

		this.typeBuilderList.put(modelName, bizTypeBuilder);

		// if has detail tables, create detail tables' type first
		List<FieldMeta> subDatasetFields = this.getAllSubDatasetField(bizCfg);
		if (subDatasetFields != null)
		{
			for (FieldMeta fldCfg : subDatasetFields)
			{
				this.createType(fldCfg.getSubDataset());
			}
		}

		configureBizObjectField(bizCfg, bizTypeBuilder);
		if (subDatasetFields != null)
		{
			// if has detail tables, configure relation
			for (FieldMeta fldCfg : subDatasetFields)
			{
				this.configureRelation(bizTypeBuilder, fldCfg);
			}
		}
		this.helper.addTypes(false, false, bizTypeBuilder.getType());
	}

	private List<FieldMeta> getAllSubDatasetField(ModelMeta bizCfg)
	{
		List<FieldMeta> result = null;
		for (FieldMeta fldObj : bizCfg.getFields())
		{
			if (FieldMetaType.SUBDATASET.equalsIgnoreCase(fldObj.getType())
					&& !bizCfg.getName().equalsIgnoreCase(fldObj.getSubDataset()))// void cycle reference
			{
				if (result == null){
					result = new ArrayList<FieldMeta>();
				}
				result.add(fldObj);
			}
		}

		return result;
	}

	private void configureBizObjectField(ModelMeta bizCfg, JPADynamicTypeBuilder bizTypeBuilder)
	{
		String dbColumn, keyColumn = null, fldName;
		boolean keyNotFound = true;
		for (FieldMeta fldCfg : bizCfg.getFields())
		{
			fldName = fldCfg.getName();
			dbColumn = fldCfg.getDbColumn();
			if (dbColumn == null)
				continue;

			if (keyNotFound && fldName.equalsIgnoreCase(bizCfg.getKeyField()))
			{
				keyColumn = dbColumn;
				keyNotFound = false;
			}
			if (FieldMetaType.SUBDATASET.equalsIgnoreCase(fldCfg.getType()))
				continue;

			bizTypeBuilder.addDirectMapping(fldName, BizFieldTypeFactory.getFieldType(fldCfg.getType()), dbColumn);
		}
		if (keyColumn == null)
		{
			logger.warn(bizCfg.getName() + " NOT configure key column!");
		}
		else
		{
			bizTypeBuilder.setPrimaryKeyFields(keyColumn);
		}
	}

	private void configureRelation(JPADynamicTypeBuilder masterBizTypeBuilder, FieldMeta masterFldCfg)
	{
		DynamicType subDatasetType = this.typeBuilderList.get(masterFldCfg.getSubDataset().toLowerCase()).getType();
		if (masterFldCfg.isOneToOne())
		{
			OneToOneMapping subMapping = masterBizTypeBuilder.addOneToOneMapping(masterFldCfg.getName(),
				subDatasetType, masterFldCfg.getSubDatasetFk());
			subMapping.setCascadeAll(true);
		}
		else
		{
			processUnidirectionalOneToManyMapping(masterBizTypeBuilder, masterFldCfg);

			// OneToManyMapping subMapping = masterBizTypeBuilder.addOneToManyMapping(masterFldCfg.getName(),
			// subDatasetType, masterFldCfg.getSubDatasetFk());
			// subMapping.setCascadeAll(true);
			// subMapping.setIndirectionPolicy(new NoIndirectionPolicy());
			// subMapping.setContainerPolicy(ContainerPolicy.buildDefaultPolicy());
			// subMapping.useCollectionClass(ArrayList.class);
			// subMapping.setIsPrivateOwned(true);
			// subMapping.dontUseIndirection();
			// subMapping.addTargetForeignKeyField

		}
	}

	protected void processUnidirectionalOneToManyMapping(JPADynamicTypeBuilder masterBizTypeBuilder,
			FieldMeta masterFldCfg)
	{
		ClassDescriptor masterDesc = masterBizTypeBuilder.getType().getDescriptor();
		String fkName = masterFldCfg.getSubDatasetFk();
		DatabaseField pkField = masterDesc.getPrimaryKeyFields().get(0);

		ClassDescriptor detailDesc = this.helper.getType(this.getTypeName(masterFldCfg.getSubDataset()))
				.getDescriptor();

		UnidirectionalOneToManyMapping mapping = new UnidirectionalOneToManyMapping();

		DynamicType subDatasetType = this.typeBuilderList.get(masterFldCfg.getSubDataset().toLowerCase()).getType();
		mapping.setAttributeName(masterFldCfg.getName());
		mapping.setReferenceClass(subDatasetType.getJavaClass());

		mapping.setIsLazy(false);

		mapping.setIsCascadeOnDeleteSetOnDatabase(true);
		mapping.setJoinFetch(ForeignReferenceMapping.OUTER_JOIN);// ?
		mapping.setBatchFetchType(BatchFetchType.EXISTS);// ?
		mapping.setIsPrivateOwned(true);
		mapping.setCascadeRemove(true);
		mapping.setCascadeAll(true);
		mapping.setIsCacheable(true);

		// Process the JoinColumn metadata.
		processUnidirectionalOneToManyTargetForeignKeyRelationship(mapping, pkField, fkName, masterDesc, detailDesc);

		masterBizTypeBuilder.addMapping(mapping);
	}

	/**
	 * INTERNAL: Process the join column(s) metadata for the owning side of a unidirectional one to many mapping. The
	 * default pk used only with single primary key entities. The processor should never get as far as to use them with
	 * entities that have a composite primary key (validation exception will be thrown).
	 */
	protected void processUnidirectionalOneToManyTargetForeignKeyRelationship(UnidirectionalOneToManyMapping mapping,
			DatabaseField pkField, String fkName, ClassDescriptor owningDescriptor, ClassDescriptor detailDesc)
	{
		pkField.setTable(owningDescriptor.getTables().iterator().next());
		DatabaseField fkField = null;
		for (DatabaseField fld : detailDesc.getFields())
		{
			if (fld.getNameForComparisons().equalsIgnoreCase(fkName))
			{
				fkField = fld;
				break;
			}
		}
		if (fkField == null)
		{
			throw new RuntimeException("Cannot foud field configuration: " + fkName);
		}
		if (!fkField.hasTableName())
		{
			fkField.setTable(detailDesc.getTables().iterator().next());
		}

		// Add target foreign key to the mapping.
		mapping.addTargetForeignKeyField(fkField, pkField);
	}

	public final StringBuilder getSelectJPQL(String boName)
	{
		StringBuilder sb = new StringBuilder("select p from ").append(boName).append(" p ");

		ModelMeta bizCfg = this.getBizModelByName(boName);
		if (bizCfg == null)
			throw new RuntimeException("Model:" + boName + " NOT found!");
		// for (BizField fld : bizCfg.getFields())
		// {
		// if (MetaFieldType.SUBDATASET.equalsIgnoreCase(fld.getType())){
		// sb.append(" join fetch p.").append(fld.getName());
		// }
		// }
		return sb;
	}

	/**
	 * 
	 * Copy data from map to entity, used in create and update entity.
	 * 
	 * @param boName business object name
	 * @param entity entity
	 * @param mapRec Map that keep changed data
	 */
	public final void copyFromMap(String boName, DynamicEntity entity, HashMap<String, Object> mapRec, boolean isUpdate)
	{
		ModelMeta boCfg = this.getBizModelByName(boName);
		String fldName;
		Object value;
		for (FieldMeta bizFld : boCfg.getFields())
		{
			if (FieldMetaType.SUBDATASET.equalsIgnoreCase(bizFld.getType()))
			{
				this.mergeSubDataset(bizFld, entity, mapRec, isUpdate);
				continue;
			}
			if (bizFld.getDbColumn() == null)
				continue;

			fldName = bizFld.getName();
			if (isUpdate && fldName.equalsIgnoreCase(boCfg.getKeyField()))// not copy key column
				continue;

			if (mapRec.containsKey(fldName))
			{
				value = mapRec.get(fldName);
			}
			else
			{
				value = bizFld.getDefaultValue();
				if (value == null)
					continue;
			}
			if (value != null && FieldMetaType.DATE.equalsIgnoreCase(bizFld.getType())
					&& value.getClass() == String.class)
			{
				value = IsoDate.stringToDate(value.toString(), IsoDate.DATE_TIME);
			}
			entity.set(fldName, value);
		}
	}

	@SuppressWarnings("unchecked")
	private void mergeSubDataset(FieldMeta subDsFld, DynamicEntity entity, HashMap<String, Object> mapRec,
			boolean isUpdate)
	{
		String fldName = subDsFld.getName();
		String subModelName = subDsFld.getSubDataset();
		List<HashMap<String, Object>> subMapRecLst = (List<HashMap<String, Object>>) (mapRec.get(fldName));
		DynamicEntity subEntity;
		String masterKeyField = subDsFld.getModelMeta().getKeyField();
		Object keyValue = entity.get(masterKeyField);
		if (!isUpdate)
		{
			List<DynamicEntity> subEntityLst = new ArrayList<DynamicEntity>();
			for (HashMap<String, Object> subMapRec : subMapRecLst)
			{
				subEntity = this.createEntity(subModelName);
				subEntity.set(subDsFld.getSubDatasetFk(), keyValue);
				this.copyFromMap(subModelName, subEntity, subMapRec, isUpdate);
				subEntityLst.add(subEntity);
			}
			entity.set(fldName, subEntityLst);
		}
		else
		{
			// TODO
		}

	}
}
